package com.thealgorithms.datastructures.hashmap.hashing;

import java.util.*;

/**
 * This class is an implementation of a hash table using linear probing It uses
 * a dynamic array to lengthen the size of the hash table when load factor > .7
 */
public class HashMapLinearProbing {

    private int hsize; // size of the hash table
    private Integer[] buckets; // array representing the table
    private Integer AVAILABLE;
    private int size; // amount of elements in the hash table

    /**
     * Constructor initializes buckets array, hsize, and creates dummy object
     * for AVAILABLE
     *
     * @param hsize the desired size of the hash map
     */
    public HashMapLinearProbing(int hsize) {
        this.buckets = new Integer[hsize];
        this.hsize = hsize;
        this.AVAILABLE = Integer.MIN_VALUE;
        this.size = 0;
    }

    /**
     * The Hash Function takes a given key and finds an index based on its data
     *
     * @param key the desired key to be converted
     * @return int an index corresponding to the key
     */
    public int hashing(int key) {
        int hash = key % hsize;
        if (hash < 0) {
            hash += hsize;
        }
        return hash;
    }

    /**
     * inserts the key into the hash map by wrapping it as an Integer object
     *
     * @param key the desired key to be inserted in the hash map
     */
    public void insertHash(int key) {
        Integer wrappedInt = key;
        int hash = hashing(key);

        if (isFull()) {
            System.out.println("Hash table is full");
            return;
        }

        for (int i = 0; i < hsize; i++) {
            if (buckets[hash] == null || buckets[hash] == AVAILABLE) {
                buckets[hash] = wrappedInt;
                size++;
                return;
            }

            if (hash + 1 < hsize) {
                hash++;
            } else {
                hash = 0;
            }
        }
    }

    /**
     * deletes a key from the hash map and adds an available placeholder
     *
     * @param key the desired key to be deleted
     */
    public void deleteHash(int key) {
        Integer wrappedInt = key;
        int hash = hashing(key);

        if (isEmpty()) {
            System.out.println("Table is empty");
            return;
        }

        for (int i = 0; i < hsize; i++) {
            if (buckets[hash] != null && buckets[hash].equals(wrappedInt)) {
                buckets[hash] = AVAILABLE;
                size--;
                return;
            }

            if (hash + 1 < hsize) {
                hash++;
            } else {
                hash = 0;
            }
        }
        System.out.println("Key " + key + " not found");
    }

    /**
     * Displays the hash table line by line
     */
    public void displayHashtable() {
        for (int i = 0; i < hsize; i++) {
            if (buckets[i] == null || buckets[i] == AVAILABLE) {
                System.out.println("Bucket " + i + ": Empty");
            } else {
                System.out.println("Bucket " + i + ": " + buckets[i].toString());
            }
        }
    }

    /**
     * Finds the index of location based on an inputed key
     *
     * @param key the desired key to be found
     * @return int the index where the key is located
     */
    public int findHash(int key) {
        Integer wrappedInt = key;
        int hash = hashing(key);

        if (isEmpty()) {
            System.out.println("Table is empty");
            return -1;
        }

        for (int i = 0; i < hsize; i++) {
            try {
                if (buckets[hash].equals(wrappedInt)) {
                    buckets[hash] = AVAILABLE;
                    return hash;
                }
            } catch (Exception E) {
            }

            if (hash + 1 < hsize) {
                hash++;
            } else {
                hash = 0;
            }
        }
        System.out.println("Key " + key + " not found");
        return -1;
    }

    private void lengthenTable() {
        buckets = Arrays.copyOf(buckets, hsize * 2);
        hsize *= 2;
        System.out.println("Table size is now: " + hsize);
    }

    /**
     * Checks the load factor of the hash table if greater than .7,
     * automatically lengthens table to prevent further collisions
     */
    public void checkLoadFactor() {
        double factor = (double) size / hsize;
        if (factor > .7) {
            System.out.println("Load factor is " + factor + ",  lengthening table");
            lengthenTable();
        } else {
            System.out.println("Load factor is " + factor);
        }
    }

    /**
     * isFull returns true if the hash map is full and false if not full
     *
     * @return boolean is Empty
     */
    public boolean isFull() {
        boolean response = true;
        for (int i = 0; i < hsize; i++) {
            if (buckets[i] == null || buckets[i] == AVAILABLE) {
                response = false;
                break;
            }
        }
        return response;
    }

    /**
     * isEmpty returns true if the hash map is empty and false if not empty
     *
     * @return boolean is Empty
     */
    public boolean isEmpty() {
        boolean response = true;
        for (int i = 0; i < hsize; i++) {
            if (buckets[i] != null) {
                response = false;
                break;
            }
        }
        return response;
    }
}
